package org.apache.maven.model.validation;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.maven.model.Build;
import org.apache.maven.model.BuildBase;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.InputLocation;
import org.apache.maven.model.InputLocationTracker;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginExecution;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.Profile;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.model.Reporting;
import org.apache.maven.model.Repository;
import org.apache.maven.model.Resource;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelProblem.Severity;
import org.apache.maven.model.building.ModelProblem.Version;
import org.apache.maven.model.building.ModelProblemCollector;
import org.apache.maven.model.building.ModelProblemCollectorRequest;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.util.StringUtils;

/**
 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
 */
@Component(role = ModelValidator.class)
public class DefaultModelValidator implements ModelValidator {

  private static final Pattern ID_REGEX = Pattern.compile("[A-Za-z0-9_\\-.]+");

  private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";

  private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;

  private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;

  public void validateRawModel(Model model, ModelBuildingRequest request, ModelProblemCollector problems) {
    Parent parent = model.getParent();
    if (parent != null) {
      validateStringNotEmpty("parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);

      validateStringNotEmpty("parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(),
          parent);

      validateStringNotEmpty("parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), parent);

      if (equals(parent.getGroupId(), model.getGroupId()) && equals(parent.getArtifactId(), model.getArtifactId())) {
        addViolation(problems, Severity.FATAL, Version.BASE, "parent.artifactId", null,
            "must be changed" + ", the parent element cannot have the same groupId:artifactId as the project.", parent);
      }
    }

    if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
      Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

      validateEnum("modelVersion", problems, Severity.ERROR, Version.V20, model.getModelVersion(), null, model,
          "4.0.0");

      validateStringNoExpression("groupId", problems, Severity.WARNING, Version.V20, model.getGroupId(), model);
      if (parent == null) {
        validateStringNotEmpty("groupId", problems, Severity.FATAL, Version.V20, model.getGroupId(), model);
      }

      validateStringNoExpression("artifactId", problems, Severity.WARNING, Version.V20, model.getArtifactId(), model);
      validateStringNotEmpty("artifactId", problems, Severity.FATAL, Version.V20, model.getArtifactId(), model);

      validateStringNoExpression("version", problems, Severity.WARNING, Version.V20, model.getVersion(), model);
      if (parent == null) {
        validateStringNotEmpty("version", problems, Severity.FATAL, Version.V20, model.getVersion(), model);
      }

      validate20RawDependencies(problems, model.getDependencies(), "dependencies.dependency", request);

      if (model.getDependencyManagement() != null) {
        validate20RawDependencies(problems, model.getDependencyManagement().getDependencies(),
            "dependencyManagement.dependencies.dependency", request);
      }

      validateRawRepositories(problems, model.getRepositories(), "repositories.repository", request);

      validateRawRepositories(problems, model.getPluginRepositories(), "pluginRepositories.pluginRepository", request);

      Build build = model.getBuild();
      if (build != null) {
        validate20RawPlugins(problems, build.getPlugins(), "build.plugins.plugin", request);

        PluginManagement mngt = build.getPluginManagement();
        if (mngt != null) {
          validate20RawPlugins(problems, mngt.getPlugins(), "build.pluginManagement.plugins.plugin", request);
        }
      }

      Set<String> profileIds = new HashSet<String>();

      for (Profile profile : model.getProfiles()) {
        String prefix = "profiles.profile[" + profile.getId() + "]";

        if (!profileIds.add(profile.getId())) {
          addViolation(problems, errOn30, Version.V20, "profiles.profile.id", null,
              "must be unique but found duplicate profile with id " + profile.getId(), profile);
        }

        validate20RawDependencies(problems, profile.getDependencies(), prefix + ".dependencies.dependency", request);

        if (profile.getDependencyManagement() != null) {
          validate20RawDependencies(problems, profile.getDependencyManagement().getDependencies(),
              prefix + ".dependencyManagement.dependencies.dependency", request);
        }

        validateRawRepositories(problems, profile.getRepositories(), prefix + ".repositories.repository", request);

        validateRawRepositories(problems, profile.getPluginRepositories(),
            prefix + ".pluginRepositories.pluginRepository", request);

        BuildBase buildBase = profile.getBuild();
        if (buildBase != null) {
          validate20RawPlugins(problems, buildBase.getPlugins(), prefix + ".plugins.plugin", request);

          PluginManagement mngt = buildBase.getPluginManagement();
          if (mngt != null) {
            validate20RawPlugins(problems, mngt.getPlugins(), prefix + ".pluginManagement.plugins.plugin", request);
          }
        }
      }
    }
  }

  private void validate20RawPlugins(ModelProblemCollector problems, List<Plugin> plugins, String prefix,
      ModelBuildingRequest request) {
    Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);

    Map<String, Plugin> index = new HashMap<String, Plugin>();

    for (Plugin plugin : plugins) {
      String key = plugin.getKey();

      Plugin existing = index.get(key);

      if (existing != null) {
        addViolation(problems, errOn31, Version.V20, prefix + ".(groupId:artifactId)", null,
            "must be unique but found duplicate declaration of plugin " + key, plugin);
      } else {
        index.put(key, plugin);
      }

      Set<String> executionIds = new HashSet<String>();

      for (PluginExecution exec : plugin.getExecutions()) {
        if (!executionIds.add(exec.getId())) {
          addViolation(problems, Severity.ERROR, Version.V20,
              prefix + "[" + plugin.getKey() + "].executions.execution.id", null,
              "must be unique but found duplicate execution with id " + exec.getId(), exec);
        }
      }
    }
  }

  public void validateEffectiveModel(Model model, ModelBuildingRequest request, ModelProblemCollector problems) {
    validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, model.getModelVersion(), model);

    validateId("groupId", problems, model.getGroupId(), model);

    validateId("artifactId", problems, model.getArtifactId(), model);

    validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, model.getPackaging(), model);

    if (!model.getModules().isEmpty()) {
      if (!"pom".equals(model.getPackaging())) {
        addViolation(problems, Severity.ERROR, Version.BASE, "packaging", null, "with value '" + model.getPackaging()
            + "' is invalid. Aggregator projects " + "require 'pom' as packaging.", model);
      }

      for (int i = 0, n = model.getModules().size(); i < n; i++) {
        String module = model.getModules().get(i);
        if (StringUtils.isBlank(module)) {
          addViolation(problems, Severity.WARNING, Version.BASE, "modules.module[" + i + "]", null,
              "has been specified without a path to the project directory.", model.getLocation("modules"));
        }
      }
    }

    validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, model.getVersion(), model);

    Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

    validateEffectiveDependencies(problems, model.getDependencies(), false, request);

    DependencyManagement mgmt = model.getDependencyManagement();
    if (mgmt != null) {
      validateEffectiveDependencies(problems, mgmt.getDependencies(), true, request);
    }

    if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
      Set<String> modules = new HashSet<String>();
      for (int i = 0, n = model.getModules().size(); i < n; i++) {
        String module = model.getModules().get(i);
        if (!modules.add(module)) {
          addViolation(problems, Severity.ERROR, Version.V20, "modules.module[" + i + "]", null,
              "specifies duplicate child module " + module, model.getLocation("modules"));
        }
      }

      Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);

      validateBannedCharacters("version", problems, errOn31, Version.V20, model.getVersion(), null, model,
          ILLEGAL_VERSION_CHARS);
      validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, model.getVersion(), null, model);

      Build build = model.getBuild();
      if (build != null) {
        for (Plugin p : build.getPlugins()) {
          validateStringNotEmpty("build.plugins.plugin.artifactId", problems, Severity.ERROR, Version.V20,
              p.getArtifactId(), p);

          validateStringNotEmpty("build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(),
              p);

          validate20PluginVersion("build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p, request);

          validateBoolean("build.plugins.plugin.inherited", problems, errOn30, Version.V20, p.getInherited(),
              p.getKey(), p);

          validateBoolean("build.plugins.plugin.extensions", problems, errOn30, Version.V20, p.getExtensions(),
              p.getKey(), p);

          validate20EffectivePluginDependencies(problems, p, request);
        }

        validate20RawResources(problems, build.getResources(), "build.resources.resource", request);

        validate20RawResources(problems, build.getTestResources(), "build.testResources.testResource", request);
      }

      Reporting reporting = model.getReporting();
      if (reporting != null) {
        for (ReportPlugin p : reporting.getPlugins()) {
          validateStringNotEmpty("reporting.plugins.plugin.artifactId", problems, Severity.ERROR, Version.V20,
              p.getArtifactId(), p);

          validateStringNotEmpty("reporting.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20,
              p.getGroupId(), p);
        }
      }

      for (Repository repository : model.getRepositories()) {
        validate20EffectiveRepository(problems, repository, "repositories.repository", request);
      }

      for (Repository repository : model.getPluginRepositories()) {
        validate20EffectiveRepository(problems, repository, "pluginRepositories.pluginRepository", request);
      }

      DistributionManagement distMgmt = model.getDistributionManagement();
      if (distMgmt != null) {
        if (distMgmt.getStatus() != null) {
          addViolation(problems, Severity.ERROR, Version.V20, "distributionManagement.status", null,
              "must not be specified.", distMgmt);
        }

        validate20EffectiveRepository(problems, distMgmt.getRepository(), "distributionManagement.repository", request);
        validate20EffectiveRepository(problems, distMgmt.getSnapshotRepository(),
            "distributionManagement.snapshotRepository", request);
      }
    }
  }

  private void validate20RawDependencies(ModelProblemCollector problems, List<Dependency> dependencies, String prefix,
      ModelBuildingRequest request) {
    Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
    Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);

    Map<String, Dependency> index = new HashMap<String, Dependency>();

    for (Dependency dependency : dependencies) {
      String key = dependency.getManagementKey();

      if ("import".equals(dependency.getScope())) {
        if (!"pom".equals(dependency.getType())) {
          addViolation(problems, Severity.WARNING, Version.V20, prefix + ".type", key,
              "must be 'pom' to import the managed dependencies.", dependency);
        } else if (StringUtils.isNotEmpty(dependency.getClassifier())) {
          addViolation(problems, errOn30, Version.V20, prefix + ".classifier", key,
              "must be empty, imported POM cannot have a classifier.", dependency);
        }
      } else if ("system".equals(dependency.getScope())) {
        String sysPath = dependency.getSystemPath();
        if (StringUtils.isNotEmpty(sysPath)) {
          if (!hasExpression(sysPath)) {
            addViolation(problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key,
                "should use a variable instead of a hard-coded path " + sysPath, dependency);
          } else if (sysPath.contains("${basedir}") || sysPath.contains("${project.basedir}")) {
            addViolation(problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key,
                "should not point at files within the project directory, " + sysPath
                    + " will be unresolvable by dependent projects",
                dependency);
          }
        }
      }

      Dependency existing = index.get(key);

      if (existing != null) {
        String msg;
        if (equals(existing.getVersion(), dependency.getVersion())) {
          msg = "duplicate declaration of version " + StringUtils.defaultString(dependency.getVersion(), "(?)");
        } else {
          msg = "version " + StringUtils.defaultString(existing.getVersion(), "(?)") + " vs "
              + StringUtils.defaultString(dependency.getVersion(), "(?)");
        }

        addViolation(problems, errOn31, Version.V20, prefix + ".(groupId:artifactId:type:classifier)", null,
            "must be unique: " + key + " -> " + msg, dependency);
      } else {
        index.put(key, dependency);
      }
    }
  }

  private void validateEffectiveDependencies(ModelProblemCollector problems, List<Dependency> dependencies,
      boolean management, ModelBuildingRequest request) {
    Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

    String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";

    for (Dependency d : dependencies) {
      validateEffectiveDependency(problems, d, management, prefix, request);

      if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
        validateBoolean(prefix + "optional", problems, errOn30, Version.V20, d.getOptional(), d.getManagementKey(), d);

        if (!management) {
          validateVersion(prefix + "version", problems, errOn30, Version.V20, d.getVersion(), d.getManagementKey(), d);

          /*
           * TODO: Extensions like Flex Mojos use custom scopes like "merged", "internal",
           * "external", etc. In order to don't break backward-compat with those, only warn but
           * don't error out.
           */
          validateEnum(prefix + "scope", problems, Severity.WARNING, Version.V20, d.getScope(), d.getManagementKey(), d,
              "provided", "compile", "runtime", "test", "system");
        }
      }
    }
  }

  private void validate20EffectivePluginDependencies(ModelProblemCollector problems, Plugin plugin,
      ModelBuildingRequest request) {
    List<Dependency> dependencies = plugin.getDependencies();

    if (!dependencies.isEmpty()) {
      String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";

      Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

      for (Dependency d : dependencies) {
        validateEffectiveDependency(problems, d, false, prefix, request);

        validateVersion(prefix + "version", problems, errOn30, Version.BASE, d.getVersion(), d.getManagementKey(), d);

        validateEnum(prefix + "scope", problems, errOn30, Version.BASE, d.getScope(), d.getManagementKey(), d,
            "compile", "runtime", "system");
      }
    }
  }

  private void validateEffectiveDependency(ModelProblemCollector problems, Dependency d, boolean management,
      String prefix, ModelBuildingRequest request) {
    validateId(prefix + "artifactId", problems, Severity.ERROR, Version.BASE, d.getArtifactId(), d.getManagementKey(),
        d);

    validateId(prefix + "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(), d);

    if (!management) {
      validateStringNotEmpty(prefix + "type", problems, Severity.ERROR, Version.BASE, d.getType(), d.getManagementKey(),
          d);

      validateStringNotEmpty(prefix + "version", problems, Severity.ERROR, Version.BASE, d.getVersion(),
          d.getManagementKey(), d);
    }

    if ("system".equals(d.getScope())) {
      String systemPath = d.getSystemPath();

      if (StringUtils.isEmpty(systemPath)) {
        addViolation(problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(), "is missing.",
            d);
      } else {
        File sysFile = new File(systemPath);
        if (!sysFile.isAbsolute()) {
          addViolation(problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
              "must specify an absolute path but is " + systemPath, d);
        } else if (!sysFile.isFile()) {
          String msg = "refers to a non-existing file " + sysFile.getAbsolutePath();
          systemPath = systemPath.replace('/', File.separatorChar).replace('\\', File.separatorChar);
          String jdkHome = request.getSystemProperties().getProperty("java.home", "") + File.separator + "..";
          if (systemPath.startsWith(jdkHome)) {
            msg += ". Please verify that you run Maven using a JDK and not just a JRE.";
          }
          addViolation(problems, Severity.WARNING, Version.BASE, prefix + "systemPath", d.getManagementKey(), msg, d);
        }
      }
    } else if (StringUtils.isNotEmpty(d.getSystemPath())) {
      addViolation(problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
          "must be omitted." + " This field may only be specified for a dependency with system scope.", d);
    }

    if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
      for (Exclusion exclusion : d.getExclusions()) {
        validateId(prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING, Version.V20,
            exclusion.getGroupId(), d.getManagementKey(), exclusion);

        validateId(prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING, Version.V20,
            exclusion.getArtifactId(), d.getManagementKey(), exclusion);
      }
    }
  }

  private void validateRawRepositories(ModelProblemCollector problems, List<Repository> repositories, String prefix,
      ModelBuildingRequest request) {
    Map<String, Repository> index = new HashMap<String, Repository>();

    for (Repository repository : repositories) {
      validateStringNotEmpty(prefix + ".id", problems, Severity.ERROR, Version.V20, repository.getId(), repository);

      validateStringNotEmpty(prefix + "[" + repository.getId() + "].url", problems, Severity.ERROR, Version.V20,
          repository.getUrl(), repository);

      String key = repository.getId();

      Repository existing = index.get(key);

      if (existing != null) {
        Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

        addViolation(problems, errOn30, Version.V20, prefix + ".id", null,
            "must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs " + repository.getUrl(),
            repository);
      } else {
        index.put(key, repository);
      }
    }
  }

  private void validate20EffectiveRepository(ModelProblemCollector problems, Repository repository, String prefix,
      ModelBuildingRequest request) {
    if (repository != null) {
      Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);

      validateBannedCharacters(prefix + ".id", problems, errOn31, Version.V20, repository.getId(), null, repository,
          ILLEGAL_REPO_ID_CHARS);

      if ("local".equals(repository.getId())) {
        addViolation(problems, errOn31, Version.V20, prefix + ".id", null,
            "must not be 'local'" + ", this identifier is reserved for the local repository"
                + ", using it for other repositories will corrupt your repository metadata.",
            repository);
      }

      if ("legacy".equals(repository.getLayout())) {
        addViolation(problems, Severity.WARNING, Version.V20, prefix + ".layout", repository.getId(),
            "uses the unsupported value 'legacy', artifact resolution might fail.", repository);
      }
    }
  }

  private void validate20RawResources(ModelProblemCollector problems, List<Resource> resources, String prefix,
      ModelBuildingRequest request) {
    Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

    for (Resource resource : resources) {
      validateStringNotEmpty(prefix + ".directory", problems, Severity.ERROR, Version.V20, resource.getDirectory(),
          resource);

      validateBoolean(prefix + ".filtering", problems, errOn30, Version.V20, resource.getFiltering(),
          resource.getDirectory(), resource);
    }
  }

  // ----------------------------------------------------------------------
  // Field validation
  // ----------------------------------------------------------------------

  private boolean validateId(String fieldName, ModelProblemCollector problems, String id,
      InputLocationTracker tracker) {
    return validateId(fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
  }

  private boolean validateId(String fieldName, ModelProblemCollector problems, Severity severity, Version version,
      String id, String sourceHint, InputLocationTracker tracker) {
    if (!validateStringNotEmpty(fieldName, problems, severity, version, id, sourceHint, tracker)) {
      return false;
    } else {
      boolean match = ID_REGEX.matcher(id).matches();
      if (!match) {
        addViolation(problems, severity, version, fieldName, sourceHint,
            "with value '" + id + "' does not match a valid id pattern.", tracker);
      }
      return match;
    }
  }

  private boolean validateStringNoExpression(String fieldName, ModelProblemCollector problems, Severity severity,
      Version version, String string, InputLocationTracker tracker) {
    if (!hasExpression(string)) {
      return true;
    }

    addViolation(problems, severity, version, fieldName, null, "contains an expression but should be a constant.",
        tracker);

    return false;
  }

  private boolean hasExpression(String value) {
    return value != null && value.contains("${");
  }

  private boolean validateStringNotEmpty(String fieldName, ModelProblemCollector problems, Severity severity,
      Version version, String string, InputLocationTracker tracker) {
    return validateStringNotEmpty(fieldName, problems, severity, version, string, null, tracker);
  }

  /**
   * Asserts:
   * <p/>
   * <ul>
   * <li><code>string != null</code>
   * <li><code>string.length > 0</code>
   * </ul>
   */
  private boolean validateStringNotEmpty(String fieldName, ModelProblemCollector problems, Severity severity,
      Version version, String string, String sourceHint, InputLocationTracker tracker) {
    if (!validateNotNull(fieldName, problems, severity, version, string, sourceHint, tracker)) {
      return false;
    }

    if (string.length() > 0) {
      return true;
    }

    addViolation(problems, severity, version, fieldName, sourceHint, "is missing.", tracker);

    return false;
  }

  /**
   * Asserts:
   * <p/>
   * <ul>
   * <li><code>string != null</code>
   * </ul>
   */
  private boolean validateNotNull(String fieldName, ModelProblemCollector problems, Severity severity, Version version,
      Object object, String sourceHint, InputLocationTracker tracker) {
    if (object != null) {
      return true;
    }

    addViolation(problems, severity, version, fieldName, sourceHint, "is missing.", tracker);

    return false;
  }

  private boolean validateBoolean(String fieldName, ModelProblemCollector problems, Severity severity, Version version,
      String string, String sourceHint, InputLocationTracker tracker) {
    if (string == null || string.length() <= 0) {
      return true;
    }

    if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
      return true;
    }

    addViolation(problems, severity, version, fieldName, sourceHint,
        "must be 'true' or 'false' but is '" + string + "'.", tracker);

    return false;
  }

  private boolean validateEnum(String fieldName, ModelProblemCollector problems, Severity severity, Version version,
      String string, String sourceHint, InputLocationTracker tracker, String... validValues) {
    if (string == null || string.length() <= 0) {
      return true;
    }

    List<String> values = Arrays.asList(validValues);

    if (values.contains(string)) {
      return true;
    }

    addViolation(problems, severity, version, fieldName, sourceHint,
        "must be one of " + values + " but is '" + string + "'.", tracker);

    return false;
  }

  private boolean validateBannedCharacters(String fieldName, ModelProblemCollector problems, Severity severity,
      Version version, String string, String sourceHint, InputLocationTracker tracker, String banned) {
    if (string != null) {
      for (int i = string.length() - 1; i >= 0; i--) {
        if (banned.indexOf(string.charAt(i)) >= 0) {
          addViolation(problems, severity, version, fieldName, sourceHint,
              "must not contain any of these characters " + banned + " but found " + string.charAt(i), tracker);
          return false;
        }
      }
    }

    return true;
  }

  private boolean validateVersion(String fieldName, ModelProblemCollector problems, Severity severity, Version version,
      String string, String sourceHint, InputLocationTracker tracker) {
    if (string == null || string.length() <= 0) {
      return true;
    }

    if (hasExpression(string)) {
      addViolation(problems, severity, version, fieldName, sourceHint,
          "must be a valid version but is '" + string + "'.", tracker);
      return false;
    }

    return validateBannedCharacters(fieldName, problems, severity, version, string, sourceHint, tracker,
        ILLEGAL_VERSION_CHARS);

  }

  private boolean validate20ProperSnapshotVersion(String fieldName, ModelProblemCollector problems, Severity severity,
      Version version, String string, String sourceHint, InputLocationTracker tracker) {
    if (string == null || string.length() <= 0) {
      return true;
    }

    if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
      addViolation(problems, severity, version, fieldName, sourceHint,
          "uses an unsupported snapshot version format" + ", should be '*-SNAPSHOT' instead.", tracker);
      return false;
    }

    return true;
  }

  private boolean validate20PluginVersion(String fieldName, ModelProblemCollector problems, String string,
      String sourceHint, InputLocationTracker tracker, ModelBuildingRequest request) {
    if (string == null) {
      // NOTE: The check for missing plugin versions is handled directly by the model builder
      return true;
    }

    Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);

    if (!validateVersion(fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
      return false;
    }

    if (string.length() <= 0 || "RELEASE".equals(string) || "LATEST".equals(string)) {
      addViolation(problems, errOn30, Version.V20, fieldName, sourceHint,
          "must be a valid version but is '" + string + "'.", tracker);
      return false;
    }

    return true;
  }

  private static void addViolation(ModelProblemCollector problems, Severity severity, Version version, String fieldName,
      String sourceHint, String message, InputLocationTracker tracker) {
    StringBuilder buffer = new StringBuilder(256);
    buffer.append('\'').append(fieldName).append('\'');

    if (sourceHint != null) {
      buffer.append(" for ").append(sourceHint);
    }

    buffer.append(' ').append(message);

    problems.add(new ModelProblemCollectorRequest(severity, version).setMessage(buffer.toString())
        .setLocation(getLocation(fieldName, tracker)));
  }

  private static InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
    InputLocation location = null;

    if (tracker != null) {
      if (fieldName != null) {
        Object key = fieldName;

        int idx = fieldName.lastIndexOf('.');
        if (idx >= 0) {
          fieldName = fieldName.substring(idx + 1);
          key = fieldName;
        }

        if (fieldName.endsWith("]")) {
          key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
          try {
            key = Integer.valueOf(key.toString());
          } catch (NumberFormatException e) {
            // use key as is
          }
        }

        location = tracker.getLocation(key);
      }

      if (location == null) {
        location = tracker.getLocation("");
      }
    }

    return location;
  }

  private static boolean equals(String s1, String s2) {
    return StringUtils.clean(s1).equals(StringUtils.clean(s2));
  }

  private static Severity getSeverity(ModelBuildingRequest request, int errorThreshold) {
    return getSeverity(request.getValidationLevel(), errorThreshold);
  }

  private static Severity getSeverity(int validationLevel, int errorThreshold) {
    if (validationLevel < errorThreshold) {
      return Severity.WARNING;
    } else {
      return Severity.ERROR;
    }
  }

}
